﻿


module TestSpinLock 

open System
open System.Threading
open System.Threading.Tasks
open System.Collections.Generic
open System.Diagnostics

type private Data = {
    Name : string
    Number : double
}

let private run (pairs : Map<string,int>) (actionFactory : string->int->Action) =
    let actions = 
        pairs
        |> Seq.map (fun kv -> actionFactory kv.Key kv.Value)
        |> Array.ofSeq

    let sw = Stopwatch.StartNew()
    Parallel.Invoke actions
    sw.ElapsedMilliseconds

let private insertWithLock (pairs : Map<string,int>) =
    let queue = new Queue<Data>()
    let syncroot = new obj()

    let createAction prefix total =
        new Action(fun() -> 
                        for index = 1 to total do
                            let name = sprintf "%s-%d" prefix index
                            lock syncroot (fun() -> queue.Enqueue <| {Name=name;Number=(double index)}))

    let timecost = run pairs createAction
    queue,timecost

// the correct way of using SpinLock in F#, see
// http://stackoverflow.com/questions/3019446/net-4-spinlock
// we can see that, we always generate reference cell to wrapper the SpinLock
// but we cannot use "!" to access it, because that will generate a copy
// so we have to use "contents" property to access the underlying property 
// directly without copying
let private insertWithSpinlock (pairs: Map<string,int>) =
    let queue = new Queue<Data>()
    let spinlock = ref <| new SpinLock()

    let createAction prefix total =
        new Action(fun() ->
                        for index = 1 to total do
                            let name = sprintf "%s-%d" prefix index

                            let lockTaken = ref false
                            try
                                spinlock.contents.Enter(lockTaken)
                                queue.Enqueue <| {Name=name;Number=(double index)}
                            finally
                                if !lockTaken then spinlock.contents.Exit() )

    let timecost = run pairs createAction
    queue,timecost

(*
// !!!!!! use "let" directly, without wrappering it in the reference cell
// !!!!!! it fails everytime, either because exceptions, or because assertion fails
let private insertWithSpinlock_ERROR (pairs: Map<string,int>) =
    let queue = new Queue<Data>()
    let spinlock = new SpinLock()

    let createAction prefix total =
        new Action(fun() ->
                        for index = 1 to total do
                            let name = sprintf "%s-%d" prefix index

                            let lockTaken = ref false
                            try
                                spinlock.Enter(lockTaken)
                                queue.Enqueue <| {Name=name;Number=(double index)}
                            finally
                                if !lockTaken then spinlock.Exit() )

    let timecost = run pairs createAction
    queue,timecost

    
// note: below codes may be INCORRECT
// seen from the MSDN, "Do not store SpinLock instances in readonly fields."
// (because the compiler will make a copy when operating on readonly fields)
// however, "let" always generate readonly variables (when captured, they become member fields)
// so the usage of SpinLock in the below codes may be WRONG
let private XXX insertWithSpinlock XXX (pairs: Map<string,int>) =
    let queue = new Queue<Data>()
    let spinlock = new SpinLock()

    let insert name number =
        let lockTaken = ref false
        try
            spinlock.Enter(lockTaken)
            queue.Enqueue <| {Name=name;Number=number}
        finally
            if !lockTaken then spinlock.Exit()

    let createAction prefix total =
        new Action(fun() ->
                        for index = 1 to total do
                            let name = sprintf "%s-%d" prefix index
                            insert name (double index))

    let actions = 
        pairs
        |> Seq.map (fun kv -> createAction kv.Key kv.Value)
        |> Array.ofSeq

    let sw = Stopwatch.StartNew()
    Parallel.Invoke actions
    queue,sw.ElapsedMilliseconds
*)

let private check title (pairs : Map<string,int>) (queue : Queue<Data>,tmcost) =
    let expectedTotal =
        pairs
        |> Seq.map (fun kv -> kv.Value)
        |> Seq.reduce Operators.(+)
    assert (queue.Count = expectedTotal)
    printfn "################### [%s]\ninsert %d items, totally cost %dms" title queue.Count tmcost

    queue
    |> Seq.countBy (fun d -> d.Name.Split('-').[0])
    |> Seq.iter (fun (prefix,realCount) ->
                                        let expectCount = pairs.[prefix]
                                        assert (realCount = expectCount)
                                        printfn "<%s> has %d items, match the expected" prefix realCount)

// the comparing result, most of the time, spinlock faster
// but there is some situations that Lock is faster from time to time
let compareSpinlockAndLock () =
        
    let pairs = 
        [|("stasi",999);("kgb",888)|]
        |> Map.ofArray

    insertWithLock pairs
    |> check "Lock" pairs

    insertWithSpinlock pairs
    |> check "SpinLock" pairs

//        insertWithSpinlock_ERROR pairs
//        |> check "ERROR" pairs

let testmain() =
    compareSpinlockAndLock()

